package com.zlyx.easy.swagger;

import static com.google.common.collect.FluentIterable.from;
import static springfox.documentation.builders.BuilderDefaults.nullToEmptyList;
import static springfox.documentation.spi.service.contexts.Orderings.pluginOrdering;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.ServletContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.zlyx.easy.core.loggers.Logger;
import com.zlyx.easy.swagger.helpers.DefaultRequestHandlerCombiner;

import springfox.documentation.RequestHandler;
import springfox.documentation.schema.AlternateTypeRule;
import springfox.documentation.schema.AlternateTypeRuleConvention;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.DocumentationPlugin;
import springfox.documentation.spi.service.RequestHandlerCombiner;
import springfox.documentation.spi.service.RequestHandlerProvider;
import springfox.documentation.spi.service.contexts.Defaults;
import springfox.documentation.spi.service.contexts.DocumentationContext;
import springfox.documentation.spi.service.contexts.DocumentationContextBuilder;
import springfox.documentation.spring.web.DocumentationCache;
import springfox.documentation.spring.web.plugins.DefaultConfiguration;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;

@Component
public class EasySwaggerBootstrapper implements SmartLifecycle {

	private static final String SPRINGFOX_DOCUMENTATION_AUTO_STARTUP = "springfox.documentation.auto-startup";
	private final DocumentationPluginsManager documentationPluginsManager;
	private final List<RequestHandlerProvider> handlerProviders;
	private final DocumentationCache scanned;
	private final EasySwaggerApiDocumentationScanner resourceListing;
	private final Environment environment;
	private final DefaultConfiguration defaultConfiguration;

	private AtomicBoolean initialized = new AtomicBoolean(false);

	@Autowired(required = false)
	private RequestHandlerCombiner combiner;

	@Autowired(required = false)
	private List<AlternateTypeRuleConvention> typeConventions;

	@Autowired
	public EasySwaggerBootstrapper(DocumentationPluginsManager documentationPluginsManager,
			List<RequestHandlerProvider> handlerProviders, DocumentationCache scanned,
			EasySwaggerApiDocumentationScanner resourceListing, TypeResolver typeResolver, Defaults defaults,
			ServletContext servletContext, Environment environment) {
		this.documentationPluginsManager = documentationPluginsManager;
		this.handlerProviders = handlerProviders;
		this.scanned = scanned;
		this.resourceListing = resourceListing;
		this.environment = environment;
		this.defaultConfiguration = new DefaultConfiguration(defaults, typeResolver, servletContext);
	}

	private DocumentationContext buildContext(DocumentationPlugin each) {
		return each.configure(defaultContextBuilder(each));
	}

	private void scanDocumentation(DocumentationContext context) {
		try {
			scanned.addDocumentation(resourceListing.scan(context));
		} catch (Exception e) {
			Logger.err(String.format("Unable to scan documentation context %s", context.getGroupName()), e);
		}
	}

	private DocumentationContextBuilder defaultContextBuilder(DocumentationPlugin plugin) {
		DocumentationType documentationType = plugin.getDocumentationType();
		List<RequestHandler> requestHandlers = from(handlerProviders).transformAndConcat(handlers()).toList();
		List<AlternateTypeRule> rules = from(nullToEmptyList(typeConventions)).transformAndConcat(toRules()).toList();
		return documentationPluginsManager.createContextBuilder(documentationType, defaultConfiguration).rules(rules)
				.requestHandlers(combiner().combine(requestHandlers));
	}

	private Function<AlternateTypeRuleConvention, List<AlternateTypeRule>> toRules() {
		return new Function<AlternateTypeRuleConvention, List<AlternateTypeRule>>() {
			@Override
			public List<AlternateTypeRule> apply(AlternateTypeRuleConvention input) {
				return input.rules();
			}
		};
	}

	private RequestHandlerCombiner combiner() {
		return Optional.fromNullable(combiner).or(new DefaultRequestHandlerCombiner());
	}

	private Function<RequestHandlerProvider, ? extends Iterable<RequestHandler>> handlers() {
		return new Function<RequestHandlerProvider, Iterable<RequestHandler>>() {
			@Override
			public Iterable<RequestHandler> apply(RequestHandlerProvider input) {
				return input.requestHandlers();
			}
		};
	}

	@Override
	public boolean isAutoStartup() {
		String autoStartupConfig = environment.getProperty(SPRINGFOX_DOCUMENTATION_AUTO_STARTUP, "true");
		return Boolean.valueOf(autoStartupConfig);
	}

	@Override
	public void stop(Runnable callback) {
		callback.run();
	}

	@Override
	public void start() {
		if (initialized.compareAndSet(false, true)) {
			Logger.info("Context refreshed");
			List<DocumentationPlugin> plugins = pluginOrdering()
					.sortedCopy(documentationPluginsManager.documentationPlugins());
			Logger.info("Found {} custom documentation plugin(s)", plugins.size());
			for (DocumentationPlugin each : plugins) {
				scanDocumentation(buildContext(each));
			}
		}
	}

	@Override
	public void stop() {
		initialized.getAndSet(false);
		scanned.clear();
	}

	@Override
	public boolean isRunning() {
		return initialized.get();
	}

	@Override
	public int getPhase() {
		return Integer.MAX_VALUE;
	}
}
